本文同步發表於個人部落格:FHIR 資源快速入門

翻開火線超人的程式碼,有一個服務叫 fhir_patient_service,工作很單純:把病人資料從 FHIR server 抓回來。火線超人後來的許多功能,查掛號、看報告、健康分析,都是從這個最基本的動作長出來的。畢竟要讓病人在 LINE 裡打一句話就看到自己的資料,第一步不是寫聊天機器人,是先搞懂 FHIR 的資料長什麼樣。
昨天說過,你可以把 FHIR server 當成一個醫療專用的資料庫。今天就用資料庫的方式將 FHIR 的三個核心概念先看一遍:Resource、Reference、Bundle。看完我們就可以動手查真的 FHIR 伺服器了。
FHIR 把醫療世界拆成一百多種 Resource,每一種都像一張定義好的資料表:Patient 存病人基本資料、Observation 存檢驗檢查數值、Condition 存診斷、MedicationRequest 存處方。一筆資料就是一份 JSON,長這樣:
{
"resourceType": "Patient",
"id": "example",
"name": [{ "family": "Chen", "given": ["Hsiao-Ming"] }],
"gender": "male",
"birthDate": "1990-01-01"
}
每份資料都有兩個欄位:resourceType 說明這是哪張表,id 是這筆資料的主鍵。想拿某一筆資料,URL 也是標準的:
GET [base]/Patient/{id}
跟資料庫不一樣的地方也先說在前面:FHIR 的欄位是巢狀 JSON,而且幾乎每個欄位都可有可無,名字可以有好幾組,性別可以沒填。
資料表之間要關聯,資料庫用外鍵,FHIR 用 Reference。一筆檢驗的 Observation 是這樣指回病人的:
{
"resourceType": "Observation",
"id": "bp-001",
"subject": { "reference": "Patient/example" }
}
subject.reference 裡的 Patient/example,就是「這筆檢驗屬於哪位病人」的外鍵。診斷指向病人,處方同時指向病人和開藥的醫師,整份醫療紀錄就是靠 Reference 織成一張網。
真實伺服器上的一筆 Observation 長這樣:

一筆體重紀錄同時掛著兩條 Reference:subject 指向這是誰的體重,encounter 指向這是哪一次就診量的。
把同一位病人的幾筆資料攤開,那張網長這樣:

每個箭頭都只是 JSON 裡的一行字串。這是它跟資料庫外鍵最大的差別:沒有 JOIN 可以一次撈完關聯,得逐一取回;資料庫也不會幫你檢查這行字串指向的資料到底存不存在,寫進去的時候擋不擋,完全看伺服器怎麼實作。我在這台測試伺服器上抽驗了 45 條 Reference,每一條都找得到對應的資料。但那是這批測試資料本身乾淨,不是伺服器給你的保證。
在一般資料庫下 SQL 的結果通常會是一個資料集,但查 FHIR 拿到的是 Bundle。我們可以對 Patient 做搜尋:
https://r4.smarthealthit.org/Patient?gender=female&_count=5
回來的不是陣列,是一個 type 為 searchset 的 Bundle:entry 裝著每一筆資源,link 裡放著下一頁的網址,total 告訴你符合條件的總共幾筆。
total 這個欄位要特別說一下:它在規格裡是選填的。伺服器算總數要付出代價,所以不少實作只在你給了搜尋條件時才回,沒條件的全表查詢就省略。
兩張圖擺在一起最清楚。先是不給條件的 Patient?_count=3:

再來是加了 gender=female 的同一支查詢:

行號對齊著看:第 7 行都是 type,但第二張的第 8 行多了 total,把後面的 link 和 entry 整個往下擠一行。待會動手時你會親眼看到這個差別,先記著「找不到 total 不代表壞掉」。
搜尋條件就寫在 query string 上,語感跟 WHERE 很接近。
整理成對照表,後端工程師應該會覺得眼熟:
| 資料庫概念 | FHIR 對應 |
|---|---|
| 資料表 | Resource type(Patient、Observation) |
| 一筆資料 | 一份 Resource(JSON) |
| 主鍵 | id |
| 外鍵 | Reference |
| SELECT 加 WHERE | search parameters |
| JOIN | _include(day20 再聊) |
| 查詢結果集 | Bundle |
先打個預防針:FHIR 是 API 標準,不是儲存引擎,伺服器後面可能接任何資料庫。但對 app 開發者來說,這張對照表已經足夠讓你開工。
今天用 SMART Health IT 提供的開放測試伺服器,裡面全是假病人資料,不用授權就能查,瀏覽器就是你的查詢工具:
兩個提醒:測試伺服器是公用的,資料可能隨時被清掉,查不到就換一筆;如果整台掛了,備用選手是 hapi.fhir.org/baseR4 ,操作方式一模一樣。
今天用資料庫的眼睛認識了 FHIR 的三個核心:Resource 是標準化的資料表,Reference 是資源之間的外鍵,Bundle 是查詢結果的包裹,而且你已經親手查過真的 FHIR server 了。
最後看一眼規模。同一位病人,把他名下所有資料一次撈出來是這樣:

一個人就牽動 16 種資源、200 筆資料,而且七成集中在 Observation。更麻煩的是「指向這個人」的欄位名不統一,有 subject、patient、for、actor 四種。
差別在於那個欄位允許指向什麼。subject 最寬鬆,可以是病人、一群受試者,甚至一台裝置或一個地點,所以它不能叫 patient;Immunization 和 Device 用 patient,因為打疫苗、貼裝置的對象一定是人,規格就鎖死了;Task 用 for,指的是這件工作的受益者,做事的人另外記在 owner;Appointment 用 actor,因為一場約診的參與者可能是病人、醫師或診間,病人只是其中一個。
所以你直接讀 JSON 想找「這是誰的資料」時,得先知道每種資源該看哪個欄位。好消息是搜尋參數把這個差異抹平了,十種資源都吃 ?patient={id},這部分 day20 談搜尋的時候再展開。
那如果連「該查哪些資源型別」都不想自己決定呢?FHIR 為此準備了 $everything。它不是搜尋,是一個「操作」,FHIR 用開頭的 $ 跟資源路徑做區分。想自己試就把上面查到的病人 id 換進去:
GET [base]/Patient/{id}/$everything
例如 https://r4.smarthealthit.org/Patient/{id}/$everything?_count=200 。這個操作有個有趣的地方:這台伺服器的 metadata 完全沒有宣告它,也就是說你沒辦法靠探詢知道它存在,只能翻規格或直接試。明天之後我們會發現,伺服器願意告訴你的能力,跟它實際支援的能力常常不是同一回事。
明天把開發環境搭起來:SMART Health IT Launcher 怎麼用、本機專案怎麼起,把 day05 開始拆授權需要的工具一次備齊。